Oggi discuteremo come strutturare la pipeline di analisi dal punto di vista logico, come gestire i dati e come automazzare le analisi con gli strumenti di automazione come Snakemake
Ritorniamo al discorso fatto nella prima lezione, riguardo all'affidabilità delle analisi ed alla loro riproducibilità.
tutto quello che viene fatto a mano è da considerarsi già dimenticato, quindi bisogna automatizzare il più possibile
Questo può essere fatto a più livelli:
Nella prima lezione abbiamo trattato come tenere traccia della storia delle nostre analisi con il controllo di versione.
Oggi approfondiremo questi concetti e li espanderemo ad altri livelli.
Per prima cosa, vediamo come strutturare i nostri dati.
In linea di principio abbiamo due macrocategorie di tipi di dati:
Questi formati di dati non sono altro che file di testo formattati in un modo particolare, per contenere informazioni leggibili dal computer.
Questi formati sono tipicamente dispendiosi in termini di spazio e velocità di lettura, ma sono anche tipicamente più robusti alla corruzione e possono essere più facilmente ispezionati da un essere umano.
Questi formati vengono poi spesso spostati in giro in versione compressa, per risparmiare spazio, e dovrebbero essere considerati formati binari. Per comodità di classificazione, farò finta di nulla. Inoltre, molte librerie come Pandas, leggono i formati testuali direttamente anche quando sono compressi.
Formati tabulari, separati da virgole o tabulazioni (o punti e virgola), uno dei formati più comuni per rappresentare le tabelle.
nome;età;abitazione
Antonio;32;Bologna
Maria;25;Torino
Francesco;47;Napoli
Permettono di inserire commenti (tipicamente con #) e possono essere aperti ed editati facilmente con programmi come Excel, ma possono contenere solo dati in formato tabella
formato standard di condivisione dati su internet, è molto utile per rappresentare dati gerarchici. Corrisponde sostanzialmente ad una dizionario di dizionari (in termini di python), ed ha praticamente la stessa sintassi.
{"persone":
{"antonio": {"età": "32", "abitazione": "Bologna"},
"maria": {"età": "20", "abitazione": "Cesena"},
}
}
I dati INI sono nati sotto l'ambiente windows per la gestione delle configurazioni. Sono molto vicini al JSON come livello di espressività e facilità di gestione.
[Sezione]
chiave_con_valore = 2
chiave_booleana
[sottosezione]
sottodettaglio = 3
[Nuova sezione]
altro_parametro = "codice hash"
Formato di configurazione molto ricco. è molto semplice da scrivere per un essere umano, ma il formato è potenzialmente molto complesso e quindi non c'è una garanzia che la libreria lo riesca ad importare correttamente se si usano costrutti un po' elaborati.
Se ci si mantiene sul semplice è però molto comodo
persone:
Antonio:
età: 30
abitazione: Ferrara
hobby: [pesca, calcio]
Maria:
età: 20
abitazione: Torino
hobby: [immersioni, paracadutismo]
Formato figlio del vecchio html, si presta bene a rappresentare dati molto complicati, ma è estremamente verboso e non è banale farne il parsing a meno che non sia indicato in un altro file quale sia la sua struttura
<listapersone>
<persona nome='Antonio'>
<annonascita>1983</annonascita>
<cittàresidenza>Bologna</cittàresidenza>
</persona>
<persona nome='Maria'>
<annonascita>1997</annonascita>
<cittàresidenza>Rimini</cittàresidenza>
</persona>
</listapersone>
molti formati sono in realtà XML, anche se non ce ne rendiamo conto:
I formati binari codificano l'informazione in modo molto compresso, sfruttando una codifica diretta dei bit.
Questi formati sono molto spesso specifici di una singola applicazione, ma esistono dei formati generali riconosciuti in vari contesti.
Di seguito ne presenterò alcuni che sono abbastanza comuni o di interesse per il nostro campo
Questi formati contengono i valori di colore contenuti in un'immagine con diversi tipi di compressione, senza però perdere alcuna informazione, al contrario di formati come il jpeg, che andrebbe quindi evitato per lo storage di immagini.
Se il vostro dato è un plot e non un'immagine generica, considerate l'uso del formato SVG, che permette modificazioni successive con più facilità.
formato medico per lo storage di dati di immagini, come radiologie, TAC, risonanze magnetiche e simili.
Contengono un gran numero di metadati sul paziente e la configurazione del macchinario.
Numpy definisce un suo formato binario, adatto a salvare un singolo array in modo molto semplice ed open.
Viste le limitazioni non è un buon formato per lo scambio di dati, ma è ottimo per file temporanei durante le analisi.
Questo è un formato generico di storage binario, adatto a dati numerici.
È il formato di default delle versioni moderne di matlab per il salvataggio dei dati (anche se li definiscono file .mat).
Sono degli interi filesystem virtuali in cui si possono contenere un numero arbitrario di matrici numeriche ed una buona quantità di metadati.
Non è adatto nel caso sia necessario fare ripetute riscritture degli stessi dati.
Performance di diversi formati di dati per la scrittura di una matrice numerica di medie dimensioni (500 x 150'000)
tipo di dato | dimensione | scrittura | lettura | compressione post |
---|---|---|---|---|
csv esteso | 430 MB | 1m 15s | 9.78s | 132 MB |
csv con gzip | 132 MB | 6m 57s | 16.2s | |
hdf5 non compresso | 463 MB | 155ms | 299ms | 154 MB |
npy non compresso | 462 MB | 638ms | 393ms | 154 MB |
Una operazione molto comune nella data analisi è il cosiddetto hashing.
Questo consiste nella generazione di una stringa alfanumerica (tipicamente di qualche centinaio di caratteri) a partire dal contenuto del file.
Gli algoritmi di hash garantiscono che piccole modifiche del contenuto del file comporti grandi variazioni dell'hash, permettendo di usare queste stringhe per garantire l'integrità dei dati.
Esistono diversi algoritmi per la generazione dell'hash (SHA1, md5, sha512, etc...) per cui l'algoritmo di calcolo dovrebbe essere riportato insieme all'hash
I sistemi di controllo di versione usano internamente gli hash dei file per verificare se questi sono cambiati o meno.
import hashlib
stringa1 = "sono una stringa di testo completamente 1nnocente".encode('utf8')
r = hashlib.md5(stringa1).hexdigest()
print('md5: {}'.format(r))
stringa1 = "sono una stringa di testo completamente lnnocente".encode('utf8')
r = hashlib.md5(stringa1).hexdigest()
print('md5: {}'.format(r))
stringa1 = "sono una stringa di testo completamente innocente".encode('utf8')
r = hashlib.md5(stringa1).hexdigest()
print('md5: {}'.format(r))
md5: 924c9da73082e7ca11ef717a8e65ac7c md5: 11d1f5de573d53f8f194e56b6de0a047 md5: b8e8b3b5490d600d8451d733d0f135f3
!md5sum ./Snakefile
fcdcdbda6cc42ee8fac608ed9e2d4391 ./Snakefile
def md5(fname):
"""funzione di hash dei file appropriata anche per i big data"""
hash_md5 = hashlib.md5()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
hash_result = md5("Snakefile")
print(hash_result)
fcdcdbda6cc42ee8fac608ed9e2d4391
Ora che abbiamo un'idea di come siano fatti i dati, possiamo discutere di come questi dati possano venir messi insieme in una pipeline.
Per semplicità, li possiamo inserire in una gerarchia di rilevanza:
Questi nomi non sono formali, solo un modo per discuterne fra di noi, ma rispecchiano molto bene la tipica procedura di analisi.
Dati a proposito dei dati.
Tipicamente un file di testo semplice, che descrive il motivo per cui i dati sono stati presi, chi li ha raccolti, quando e con che metodi.
Può sembrare banale, ma a distanza di qualche anno potreste trovarvi un disco pieno di dati di cui non capite la ragione d'essere.
Un dato di cui non si conosce il motivo di esistere ha lo stesso valore di un dato cancellato.
Spesso riporta anche l'hashing dei dati RAW per garantire di poterne verificare l'integrità.
Questi sono i dati originari dei vostri esperimenti. Non li userete direttamente nelle vostre analisi. Potrebbero essere nei formati più strampalati, a seconda dell'output dello strumento di misura.
Questi dati sono SACRI.
Vanno conservati scrupolosamente e non modificati.
Se si dovessero avere nuove versioni (ad esempio una misura è stata ripetuta ed aggiornata) non sovrascriveteli, ma tenete le varie versioni.
Dal punto di vista di FOSSIL per il controllo di versione, questo tipo di dati può essere tenuto nel repository come unversioned, visto che non dovrebbe essere modificato.
Può sembrare banale, ma il vostro codice rappresenta comunque informazione a proposito del problema:
Queste informazioni meritano di essere trattate come dati a tutti gli effetti, e sono quasi altrettanto importanti dei raw data.
È per questo che esiste il controllo di versione, usatelo!
Una volta ottenuti i raw data, processateli tramite uno script in un formato decente. Tramite questo script potrete unire eventuali file frammentati (ad esempio da un file per persona ad un dataset unico), correggere errori nei raw data (non dovete andare a modificarli alla fonte!) e mettere il tutto in una struttura che possa essere mantenuta a lungo. La mia preferenza è per formati di testo come il csv.
Lo script di generazione è più importante dei dati risultanti!
In questa fase dovete mantenere i dati il più possibile aderenti alle informazioni dei file RAW. Non fate detrending, normalizzazioni o altre modifiche, solo trasportarli in un formato ragionevole.
Se i RAW fossero già ben formattati e strutturati e corretti, potete considerarli direttamente dei source. Non capita mai, non sperateci. Quando capita di solito vi stanno mentendo e non sono davvero i RAW ma dei dati già processati.
Qui iniziamo effettivamente la nostra analisi.
A partire dai dati source, possiamo comporre i dati nel formato più comodo per le analisi che abbiamo in mente, senza doverci preoccupare di cose come la normalizzazione delle tabelle del database e simili.
Tipicamente questi dati saranno generati solo una volta, a meno che non si riscontrino dei problemi nell'analisi (nel qual caso sarà necessario tornare indietro ai source data e capire cosa sia andato storto).
Diverse analisi potrebbero aver bisogno di diversi usage data.
La vostra analisi sarà tipicamente composta da diverse fasi, quali la normalizzazione dei dati, il detrending, e così via.
Dopo ciascuno di questi step è solitamente buona pratica tener traccia dei risultati in dei file intermedi, in modo da poter riprendere l'analisi da qualsiasi step intermedio senza dover ripartire da capo.
Di solito l'unico inconveniente della perdita di questi dati è il tempo necessario per ripetere l'analisi.
Questi dati sono il risultato intermedio delle analisi, ma con il preciso obiettivo di essere rimossi alla fine di ogni step.
Ad esempio potrebbero essere generati da un algoritmo parallelo distribuito che analizza un paziente alla volta, per poi ricomporre i risultati in un'unica tabella che li contiene tutti. Una volta generata la tabella complessiva, non c'è ragione di tenere le tabelle dei singoli pazienti in giro a consumare spazio, e vanno quindi eliminate.
Snakemake è una reimplementazione in python del classico programma UNIX make, usato per la compilazione del codice.
Snakemake rende l'automazione delle pipeline di analisi estremamente comoda e permette di scalare facilmente da sequenziale a parallelismo locale fino a calcolo su cluster.
Vi permette di collegare in modo molto semplice comandi di bash (e quindi programmi generici), script in python ed R (a cui può passare variabili direttamente in memoria) o di mescolare del codice python direttamente nello script.
Snakamake è solo uno dei tanti sistemi di gestione delle pipeline di dati.
È quello che reputo più vicino ai nostri bisogni, ma vi invito a guardare anche gli altri.
Alcuni esempi famosi sono:
una lista molto comprensiva la trovate alla pagina awesome-pipeline
La struttura fondamentale di Snakemake è una regola, che rappresenta un programma (tipicamente uno script), i file che ha bisogno di avere in input e quelli che restituirà in output.
Ciascuna regola viene eseguita come un nuovo processo python a se stante.
Una regola ha delle sotto sezioni, di cui le più importanti sono:
Di default, se non si specifica altro, snakemake cerca di eseguire la regola all
Se il file di output della regola esiste già, la regola non viene eseguita (a meno di non constringerlo).
Se il file di input della regola non esiste, snakemake cerca un'altra regola che abbia quel file come output e la esegue prima.
Questa è una struttura chiamata pull (in cui specifico il punto di arrivo), e richiede un po' di tempo per prenderci confidenza (lo standard della programmazione è di tipo push, in cui specifico il punto di partenza).
%%file Snakefile
rule all:
shell:
"echo 'hello world' > result.txt"
Overwriting Snakefile
!snakemake
Provided cores: 1 Rules claiming more threads will be scaled down. Job counts: count jobs 1 all 1 rule all: jobid: 0 Finished job 0. 1 of 1 steps (100%) done
%ls
result.txt Snakefile
Se il file di output di una regola esiste già, ed è più recente dei file di input, la regola non viene eseguita.
Questo comportamento è detto idempotenza, e rende l'esecuzione dello script più prevedibile.
È comunque possibile forzare la mano a snakamake in vari modi (se vi servisse, li trovate sul manuale)
%%file Snakefile
rule all:
output:
"result.txt"
shell:
"echo 'hello world' > {output}"
Overwriting Snakefile
!snakemake
Nothing to be done.
%rm result.txt
!snakemake
Provided cores: 1 Rules claiming more threads will be scaled down. Job counts: count jobs 1 all 1 rule all: output: result.txt jobid: 0 Finished job 0. 1 of 1 steps (100%) done
Se i fil di input sono specificati e:
Allora snakemake ritornerà un errore
%%file Snakefile
rule all:
input:
"parziali1.txt",
"parziali2.txt"
output:
"result.txt"
shell:
"cat {input} > {output}"
Overwriting Snakefile
!snakemake
MissingInputException in line 2 of /home/enrico/lavoro/snakemake_lesson/Snakefile:
Missing input files for rule all:
parziali2.txt
parziali1.txt
Vediamo come appare uno script con due regole distinte, una per creare i due parziali ed una per processarli.
%%file Snakefile
rule all:
input:
"parziali1.txt",
"parziali2.txt"
output:
"result.txt"
shell:
"cat {input} > {output}"
rule crea_parziali:
output:
"parziali1.txt",
"parziali2.txt"
run:
for filename in output:
with open(filename, 'w') as file:
print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
!snakemake
Provided cores: 1 Rules claiming more threads will be scaled down. Job counts: count jobs 1 all 1 crea_parziali 2 rule crea_parziali: output: parziali1.txt, parziali2.txt jobid: 1 Finished job 1. 1 of 2 steps (50%) done rule all: input: parziali1.txt, parziali2.txt output: result.txt jobid: 0 Finished job 0. 2 of 2 steps (100%) done
%ls
parziali1.txt parziali2.txt result.txt Snakefile
%cat parziali1.txt
risultato di parziali1.txt
%cat parziali2.txt
risultato di parziali2.txt
%cat result.txt
risultato di parziali1.txt risultato di parziali2.txt
Se i risultati intermedi esistono già, non li esegue di nuovo
%rm result.txt
!snakemake
Provided cores: 1 Rules claiming more threads will be scaled down. Job counts: count jobs 1 all 1 rule all: input: parziali1.txt, parziali2.txt output: result.txt jobid: 0 Finished job 0. 1 of 1 steps (100%) done
In questo script la stessa funzione python crea tutti i file uno alla volta, in modo indipendente.
In realtà potrei eseguirlo in modo concorrente, senza doversi aspettare. Per fare questo posso usare le wildcards.
Possono provocare torsioni della materia grigia, ma atteniamoci al caso più semplice
%%file Snakefile
rule all:
input:
"parziali{numero}.txt"
output:
"result.txt"
shell:
"cat {input} > {output}"
rule crea_parziali:
output:
"parziali{numero}.txt"
run:
with open(output, 'w') as file:
print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
!snakemake
WorkflowError in line 2 of /home/enrico/lavoro/snakemake_lesson/Snakefile:
Wildcards in input files cannot be determined from output files:
'numero'
Per usare le wildcard devo dare da qualche parte il comando expand, che assegna di vari possibili valori alle wildcards.
Posso avere più wildcards allo stesso momento, l'importante è inizializarle tutte.
Ci sono dei meccanismi per fare inferenza automatica delle wildcard, ma vi consiglio di prendere prima confidenza con la dichiarazione esplicita.
%%file Snakefile
numeri = [1, 2, 3, 4]
rule all:
input:
expand("parziali{numero}.txt", numero=numeri)
output:
"result.txt"
shell:
"cat {input} > {output}"
rule crea_parziali:
output:
out = "parziali{numero}.txt"
run:
filename = output.out
with open(filename, 'w') as file:
print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
%rm result.txt
%ls
parziali1.txt parziali2.txt Snakefile
!snakemake
Provided cores: 1 Rules claiming more threads will be scaled down. Job counts: count jobs 1 all 2 crea_parziali 3 rule crea_parziali: output: parziali3.txt jobid: 3 wildcards: numero=3 Finished job 3. 1 of 3 steps (33%) done rule crea_parziali: output: parziali4.txt jobid: 1 wildcards: numero=4 Finished job 1. 2 of 3 steps (67%) done rule all: input: parziali1.txt, parziali2.txt, parziali3.txt, parziali4.txt output: result.txt jobid: 0 Finished job 0. 3 of 3 steps (100%) done
%cat result.txt
risultato di parziali1.txt risultato di parziali2.txt risultato di parziali3.txt risultato di parziali4.txt
%rm parziali1.txt
%rm result.txt
Snakamake mi permette anche di creare un grafico di flusso che visualizza tutto ciò che deve essere fatto, e ciò che invece è stato già fatto e non ha bisogno di una nuova esecuzione.
!snakemake --dag | dot -Tsvg > dag.svg
Un'altra funzione estramamente utile è la creazione di un registro di provenance, che mi indica quali file sono stati creati da quale regola e con che parametri.
Questo permette di tenere traccia dell'origine di ciascun file in modo semplice.
È anche facile impostarlo in modo da appendere la provenance ad un log completo, dando così la storia di tutti i file creati e modificati nel tempo.
!snakemake --detailed-summary > provenance.tsv
import pandas as pd
pd.read_table("provenance.tsv", index_col=0)
date | rule | version | log-file(s) | input-file(s) | shellcmd | status | plan | |
---|---|---|---|---|---|---|---|---|
output_file | ||||||||
result.txt | - | all | - | NaN | parziali1.txt,parziali2.txt,parziali3.txt,parz... | cat parziali1.txt parziali2.txt parziali3.txt ... | missing | update pending |
parziali2.txt | Sat Mar 11 12:59:59 2017 | crea_parziali | - | NaN | NaN | - | rule implementation changed | no update |
parziali1.txt | - | crea_parziali | - | NaN | NaN | - | missing | update pending |
parziali4.txt | Sat Mar 11 13:10:21 2017 | crea_parziali | - | NaN | NaN | - | ok | no update |
parziali3.txt | Sat Mar 11 13:10:21 2017 | crea_parziali | - | NaN | NaN | - | ok | no update |
%rm *.txt
rm: cannot remove ‘*.txt’: No such file or directory dag.svg provenance.tsv Snakefile
Se voglio eseguire più regole in parallelo (ovviamente rispettando l'ordine necessario di esecuzione di ciascun ramo), mi basta dare il comando --cores <N>
e snakemake eseguirà in automatico tutto quello che riesce in parallelo.
Esiste un equivalente anche per lanciare la pipeline in un cluster di calcolo, rendendo molto semplice il calcolo distribuito.
!snakemake --cores 6
Provided cores: 6 Rules claiming more threads will be scaled down. Job counts: count jobs 1 all 4 crea_parziali 5 rule crea_parziali: output: parziali1.txt jobid: 3 wildcards: numero=1 rule crea_parziali: output: parziali3.txt jobid: 4 wildcards: numero=3 rule crea_parziali: output: parziali4.txt jobid: 1 wildcards: numero=4 rule crea_parziali: output: parziali2.txt jobid: 2 wildcards: numero=2 Finished job 3. 1 of 5 steps (20%) done Finished job 1. 2 of 5 steps (40%) done Finished job 2. 3 of 5 steps (60%) done Finished job 4. 4 of 5 steps (80%) done rule all: input: parziali1.txt, parziali2.txt, parziali3.txt, parziali4.txt output: result.txt jobid: 0 Finished job 0. 5 of 5 steps (100%) done
%rm *.txt
Posso anche specificare delle risorse limitate (oltre i processori) in modo che la pipeline non ecceda nell'uso.
Ad esempio, se ho delle regole che richiedono una gran quantità di memoria, posso specificare il livello atteso di occupazione nella regola e poi specificare la memoria disponibile da linea di comando.
%%file Snakefile
numeri = [1, 2, 3, 4]
rule all:
input:
expand("parziali{numero}.txt", numero=numeri)
output:
"result.txt"
shell:
"cat {input} > {output}"
rule crea_parziali:
output:
out = "parziali{numero}.txt"
resources:
memory = 6
run:
filename = output.out
with open(filename, 'w') as file:
print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
!snakemake --cores 6 --resources memory=12
Provided cores: 6 Rules claiming more threads will be scaled down. Provided resources: memory=12 Job counts: count jobs 1 all 4 crea_parziali 5 rule crea_parziali: output: parziali2.txt jobid: 2 wildcards: numero=2 resources: memory=6 rule crea_parziali: output: parziali4.txt jobid: 1 wildcards: numero=4 resources: memory=6 Finished job 2. 1 of 5 steps (20%) done rule crea_parziali: output: parziali3.txt jobid: 3 wildcards: numero=3 resources: memory=6 Finished job 1. 2 of 5 steps (40%) done rule crea_parziali: output: parziali1.txt jobid: 4 wildcards: numero=1 resources: memory=6 Finished job 3. 3 of 5 steps (60%) done Finished job 4. 4 of 5 steps (80%) done rule all: input: parziali1.txt, parziali2.txt, parziali3.txt, parziali4.txt output: result.txt jobid: 0 Finished job 0. 5 of 5 steps (100%) done
Eventuali parametri di configurazione possono essere dati da linea di comando oppure caricati da un file di configurazione in formato YAML o JSON
%%file Snakefile
numeri = [i for i in range(int(config['number']))]
rule all:
input:
expand("parziali{numero}.txt", numero=numeri)
output:
"result.txt"
shell:
"cat {input} > {output}"
rule crea_parziali:
output:
out = "parziali{numero}.txt"
resources:
memory = 6
run:
filename = output.out
with open(filename, 'w') as file:
print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
%rm *.txt
!snakemake --cores 6 --resources memory=12 --config number=4
Provided cores: 6 Rules claiming more threads will be scaled down. Provided resources: memory=12 Job counts: count jobs 1 all 4 crea_parziali 5 rule crea_parziali: output: parziali1.txt jobid: 4 wildcards: numero=1 resources: memory=6 rule crea_parziali: output: parziali2.txt jobid: 2 wildcards: numero=2 resources: memory=6 Finished job 2. 1 of 5 steps (20%) done rule crea_parziali: output: parziali3.txt jobid: 3 wildcards: numero=3 resources: memory=6 Finished job 4. 2 of 5 steps (40%) done rule crea_parziali: output: parziali0.txt jobid: 1 wildcards: numero=0 resources: memory=6 Finished job 3. 3 of 5 steps (60%) done Finished job 1. 4 of 5 steps (80%) done rule all: input: parziali0.txt, parziali1.txt, parziali2.txt, parziali3.txt output: result.txt jobid: 0 Finished job 0. 5 of 5 steps (100%) done
%%file config.yaml
number: 4
Overwriting config.yaml
%%file Snakefile
configfile: "./config.yaml"
numeri = [i for i in range(int(config['number']))]
rule all:
input:
expand("parziali{numero}.txt", numero=numeri)
output:
"result.txt"
shell:
"cat {input} > {output}"
rule crea_parziali:
output:
out = "parziali{numero}.txt"
resources:
memory = 6
run:
filename = output.out
with open(filename, 'w') as file:
print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
%rm *.txt
!snakemake --cores 6 --resources memory=12
Provided cores: 6 Rules claiming more threads will be scaled down. Provided resources: memory=12 Job counts: count jobs 1 all 4 crea_parziali 5 rule crea_parziali: output: parziali0.txt jobid: 4 wildcards: numero=0 resources: memory=6 rule crea_parziali: output: parziali3.txt jobid: 3 wildcards: numero=3 resources: memory=6 Finished job 3. 1 of 5 steps (20%) done rule crea_parziali: output: parziali2.txt jobid: 1 wildcards: numero=2 resources: memory=6 Finished job 4. 2 of 5 steps (40%) done rule crea_parziali: output: parziali1.txt jobid: 2 wildcards: numero=1 resources: memory=6 Finished job 1. 3 of 5 steps (60%) done Finished job 2. 4 of 5 steps (80%) done rule all: input: parziali0.txt, parziali1.txt, parziali2.txt, parziali3.txt output: result.txt jobid: 0 Finished job 0. 5 of 5 steps (100%) done
Nel sito trovate un link a dei file per questa lezione, ciascuno con dentro una semplice tabella che indica una sequenza di versamenti fatti da delle persone.
Ci sarà anche un file che indica gli hash md5 per ciascuno di questi file.
Scrivete una pipeline che li scarichi, controlli che la md5 hash è quella attesa, caricate i dati e scrivete in un file il totale risultante per ciascuna persona.
https://chiselapp.com/user/EnricoGiampieri/repository/DataProgrammingCourse/doc/tip/snakemake_exercise/
md5sums.tsv
md5sum
wget
oppure da python con la libreria requests